/**
 * Licensed under the Apache License Version 2.0.
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations
 * under the License.
 */
package net.interknit.knitter.cg.cfg;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import net.interknit.knitter.cg.CGMain;
import net.interknit.knitter.cg.util.ReflectionHelper;

import org.jdom.Document;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;


/**
 *
 */
public class CGConfig
{
	public enum RequestParamType
	{
		PATH, QUERY, FORM, HEADER;

		public String annotation()
		{
			if(this.equals(PATH))
				return "@PathParam";
			else if(this.equals(QUERY))
				return "@QueryParam";
			else if(this.equals(FORM))
				return "@FormParam";
			else if(this.equals(HEADER))
				return "@HeaderParam";
			else
				throw new RuntimeException(
						"Invalid RequestParamType annotation value: " + this);
		}

		public static RequestParamType parse(String value)
		{
			try
			{
				return valueOf(value);
			}
			catch(Exception e)
			{
				return PATH;
			}
		}
	}

	private CGConfigRootEl cgconfigRoot = new CGConfigRootEl();

	// caches
	private List<String> targetClassNames = null;
	private List<String> typeClassNames = null;
	private List<String> ignoredTypes = null;
	private List<String> excludedTypes = null;

	public CGConfig()
	{
		;
	}

	public void build(String cgconfigPath)
	{
		build(cgconfigPath, null, null, null);
	}

	public void build(String cgconfigPath, String basePackageName, String outputSourceDir, String authorName)
	{
		try
		{
			InputStream is = null;

			File cgconfigFile = new File(cgconfigPath);

			// first try to see if cgconfigPath is a valid readable file
			if(cgconfigFile.exists() && ! cgconfigFile.isDirectory() && cgconfigFile.canRead())
			{
				// if so, read it
				is = new FileInputStream(cgconfigFile);
			}
			else
			{
				// otherwise, attempt to read it as a classpath resource, relative to this class
				is = getClass().getClassLoader().getResourceAsStream(cgconfigPath);
			}

			// if not found, throw FileNotFoundException
			if(is == null)
				throw new FileNotFoundException(cgconfigPath + " could not be found on path or classpath");

			// build cgconfig XML document
			SAXBuilder sax = new SAXBuilder();
			Document configDoc = sax.build(is);

			// finally, create the code-generation configuration
			cgconfigRoot = new CGConfigRootEl(configDoc.getRootElement());

			// if basePackageName is provided as a code-generation input (command line or ant task), use
			// that value to override the basepackagename attribute of cgconfig.xml
			if(basePackageName != null)
			{
				cgconfigRoot.setBasePackageName(basePackageName);
			}

			// if outputSourceDir is provided as a code-generation input (command line or ant task), use
			// that value to override the srcdir attribute of cgconfig.xml
			if(outputSourceDir != null)
			{
				cgconfigRoot.setRootSourceDirectory(outputSourceDir);
			}

			// if authorName is provided as a code-generation input (command line or ant task), use
			// that value to override the author attribute of cgconfig.xml
			if(authorName != null)
			{
				cgconfigRoot.setAuthor(authorName);
			}
		}
		catch(Exception e)
		{
			throw new RuntimeException(e);
		}
	}

	public String[] getClasspathEntries()
	{
		return cgconfigRoot.getClasspath().getClasspathEntries();
	}

	public List<String> getTargetClassNames()
	{
		if(targetClassNames != null) return targetClassNames;

		targetClassNames = new ArrayList<String>();

		for(TargetEl target : cgconfigRoot.getTargets())
			targetClassNames.add(target.getClassName());

		return targetClassNames;
	}

	public String getBasePackageName()
	{
		return cgconfigRoot.getBasePackageName();
	}

	public String getRootSourceDirectory()
	{
		return cgconfigRoot.getRootSourceDirectory();
	}

	public Properties getProperties(String targetClassName)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return new Properties();

		PropertiesEl properties = targetEl.getProperties();

		if(properties != null) return properties.getProperties();
		else return new Properties();
	}

	public String getProperty(String targetClassName, String key)
	{
		return getProperties(targetClassName).getProperty(key, "");
	}

	public String getRestName(String targetClassName)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		return targetEl.getRestName();
	}

	public List<String> getIncludedParentClassNames(String targetClassName)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		ParentsEl parents = targetEl.getParents();
		if(parents != null) return parents.getIncludedClasses();
		else return new ArrayList<String>();
	}

	public boolean includeParentClass(String targetClassName, String parentClassName)
	{
		return getIncludedParentClassNames(targetClassName).contains(parentClassName);
	}

	public List<String> getTypeClassNames()
	{
		if(typeClassNames != null) return typeClassNames;

		typeClassNames = new ArrayList<String>();

		for(TypeEl type : cgconfigRoot.getTypes())
			typeClassNames.add(type.getClassName());

		return typeClassNames;
	}

	public String getTypeRestName(String typeClassName)
	{
		for(TypeEl type : cgconfigRoot.getTypes())
		{
			if(type.getClassName().equals(typeClassName))
			{
				// type rest name (@XmlRootElement value) cannot be empty
				// however target rest name (@Path value) can be empty
				String restName = type.getRestName();
				if(restName != null && ! "".equals(restName))
					return restName;
			}
		}

		return null;
	}

	public boolean isTypeIgnored(Class<?> clazz)
	{
		String rootComponentClassName =
				ReflectionHelper.getRootComponentClass(clazz).getName();

		List<String> allIgnoredTypes = new ArrayList<String>();

		allIgnoredTypes.addAll(getIgnoredTypes());
		allIgnoredTypes.addAll(TypeEl.ignoredTypes);
		allIgnoredTypes.addAll(CGMain.getInstance().getIgnoredTypes());

		if(allIgnoredTypes.contains(rootComponentClassName))
		{
			CGMain.getInstance().log(clazz.getName() + " will be ignored");
			return true;
		}

		return false;
	}

	public boolean isTypeExcluded(Class<?> clazz)
	{
		String rootComponentClassName =
				ReflectionHelper.getRootComponentClass(clazz).getName();

		List<String> allExcludedTypes = new ArrayList<String>();

		allExcludedTypes.addAll(getExcludedTypes());
		allExcludedTypes.addAll(TypeEl.excludedTypes);
		allExcludedTypes.addAll(CGMain.getInstance().getExcludedTypes());

		if(allExcludedTypes.contains(rootComponentClassName))
		{
			CGMain.getInstance().log(clazz.getName() + " will be excluded");
			return true;
		}

		return false;
	}

	public List<String> getIgnoredTypes()
	{
		if(ignoredTypes != null) return ignoredTypes;

		ignoredTypes = new ArrayList<String>();

		for(TypeEl type : cgconfigRoot.getTypes())
			if(type.isIgnore()) ignoredTypes.add(type.getClassName());

		return ignoredTypes;
	}

	public List<String> getExcludedTypes()
	{
		if(excludedTypes != null) return excludedTypes;

		excludedTypes = new ArrayList<String>();

		for(TypeEl type : cgconfigRoot.getTypes())
			if(type.isExclude()) excludedTypes.add(type.getClassName());

		return excludedTypes;
	}

	public List<String> getMethodNames(String targetClassName)
	{
		List<String> methodNames = new ArrayList<String>();

		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				methodNames.add(method.getMethodName());

		return methodNames;
	}

	public boolean methodEquals(
			String methodName, String[] parameterClassNames,
			String otherMethodName, String[] otherParameterClassNames)
	{
		if(methodName.equals(otherMethodName))
		{
			if(parameterClassNames == null && otherParameterClassNames == null)
				return true;
			if(parameterClassNames != null && otherParameterClassNames != null)
			{
				if(parameterClassNames.length == otherParameterClassNames.length)
				{
					for(int i = 0; i < parameterClassNames.length; i++)
						if(! parameterClassNames[i].equals(otherParameterClassNames[i]))
							return false;

					return true;
				}

				return false;
			}

			return false;
		}

		return false;
	}

	public boolean methodExists(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return false;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), method.getParameters()))
					return true;

		return false;
	}

	public boolean excludeMethod(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return false;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return method.isExclude();

		return false;
	}

	public String getMethodRestName(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return method.getRestName();

		return null;
	}

	public RequestParamType getMethodRequestParamType(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return RequestParamType.PATH;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
		{
			for(MethodEl method : methods)
			{
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
				{
					return method.getRequestParamType();
				}
			}
		}

		return RequestParamType.PATH;
	}

	public String[] getMethodParameterName(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return new String[0];

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return method.getParameterNames();

		return new String[0];
	}

	public String getMethodConsumes(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return method.getConsumes();

		return null;
	}

	public String getMethodProduces(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return method.getProduces();

		return null;
	}

	public String getMethodRestMethod(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return (method.getRestMethod() != null ?
							method.getRestMethod().name() : null);

		return null;
	}

	public String getTargetComments(String targetClassName)
	{
		for(TargetEl target : cgconfigRoot.getTargets())
		{
			if(target.getClassName().equals(targetClassName))
			{
				return target.getComments();
			}
		}

		return "";
	}

	public String getMethodComments(String targetClassName, String methodName, String[] parameterClassNames)
	{
		TargetEl targetEl = cgconfigRoot.getTarget(targetClassName);

		if(targetEl == null) return null;

		List<MethodEl> methods = targetEl.getMethods();
		if(methods != null)
			for(MethodEl method : methods)
				if(methodEquals(
						methodName, parameterClassNames,
						method.getMethodName(), parameterClassNames))
					return method.getComments();

		return "";
	}

	public String getTypeComments(String typeClassName)
	{
		for(TypeEl type : cgconfigRoot.getTypes())
		{
			if(type.getClassName().equals(typeClassName))
			{
				return type.getComments();
			}
		}

		return "";
	}

	public String getTypePropertyComment(String typeClassName, String propertyName)
	{
		for(TypeEl type : cgconfigRoot.getTypes())
		{
			if(type.getClassName().equals(typeClassName))
			{
				return type.getPropertyComments(propertyName);
			}
		}

		return "";
	}

	public String getAuthor()
	{
		return cgconfigRoot.getAuthor();
	}

	@Override
	public String toString()
	{
		String raw = cgconfigRoot.toString();

		try
		{
			SAXBuilder sax = new SAXBuilder();
			Document doc = sax.build(new StringReader(raw));

			return new XMLOutputter(Format.getPrettyFormat()).outputString(doc);
		}
		catch(Exception e)
		{
			e.printStackTrace();
			return raw;
		}
	}
}
